package es.upm.gib.mappingAPI2.generate;

import es.upm.gib.mappingAPI2.*;
import es.upm.gib.owlbasicmodel2.OWLBasicModel2;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

import java.io.*;
import java.util.*;

/**
 * This class allows generating automatically views for a physical schema (or in general any RDF schema).
 * <p/>
 * The code generates all possible views from a model class and property set. The user can partly guide the process and specify or
 * generalize the generated views.
 * <p/>
 * Generated views have the following format: one main path, and optional secondary paths (called branches) coming out of it. Branches themselves
 * cannot have additional branches. At one point of the main path, only one branch max can come out.
 * <p/>
 * Elements in the main path and the branches can also include restrictions or identifier tags. The user must specify this.
 * <p/>
 * The class works with an iteration schema: there is a current view which can be retrieved, and there are different functions to move to the
 * next view, until the returned view is null. The current view can be
 * - Accepted: the code will generate the next view using the same template
 * - Discarded: same as above
 * - Specified: the code will generate a more specific view, either by making the main path longer, or by adding or growing a branch from the
 * main path. The current view will be considered as discarded
 * - Generalized: the code will generate a more general view, either by shorting the main path, or by shorting/removing an existing branch.
 * The current view will be considered as discarded
 * - Restricted or identified: one element in the current view is added a restriction, or marked as identifier. When moving to a next view,
 * these annotations will be removed
 * <p/>
 * The class also provides the ability of assessing the estimated progress (in percentage) of the process of generating all possible views of
 * the provided model
 * <p/>
 * todo serialize state to keep work in different sessions
 * todo skip to perform later. We can put the skip views in a bag and simply show them later. But we need to be able to modify them...
 */
public class PhysicalViewGenerator {

    private static class CurrentView {

        private class ListAndPointer<E> {

            private List<E> list;

            private int pointer;

            private ListAndPointer(List<E> list, List<E>... moreLists) {
                this.list = list;
                for (List<E> anotherList : moreLists) {
                    this.list.addAll(anotherList);
                }
                pointer = 0;
            }

            private ListAndPointer(E element) {
                list = new ArrayList<E>();
                list.add(element);
                pointer = 0;
            }

            private boolean hasMore() {
                return pointer < list.size() - 1;
            }

            private E getElement() {
                return list.get(pointer);
            }

            private void next() {
                pointer++;
            }

            @Override
            public String toString() {
                String str = "{";
                for (E e : list) {
                    str += e.toString() + ", ";
                }
                return str + "}, Ptr = " + pointer;
            }
        }

        private class ElementStack {

            private List<Integer> branches;

            private ElementStack() {
                branches = new ArrayList<Integer>();
            }

            private void push(int branch) {
                branches.add(branch);
            }

            private int top() {
                if (!branches.isEmpty()) {
                    return branches.get(branches.size() - 1);
                } else {
                    return -1;
                }
            }

            private void remove(int branch) {
                branches.remove(new Integer(branch));
            }

            private void clear() {
                branches.clear();
            }
        }

        private class Restrictions {

            private Map<Integer, Restriction> mainPathRestrictions;

            private Map<Integer, Map<Integer, Restriction>> branchRestrictions;

            private Restrictions() {
                mainPathRestrictions = new HashMap<Integer, Restriction>();
                branchRestrictions = new HashMap<Integer, Map<Integer, Restriction>>();
            }

            private void clear() {
                mainPathRestrictions.clear();
                branchRestrictions.clear();
            }

            private void clear(int position) {
                mainPathRestrictions.remove(position);
            }

            private void clear(int position, int branchElement) {
                branchRestrictions.get(position).remove(branchElement);
            }

            private void addRestriction(int elementPosition, Restriction restriction) {
                mainPathRestrictions.put(elementPosition, restriction);
            }

            private void addRestriction(int elementPosition, int branchElement, Restriction restriction) {
                if (!branchRestrictions.containsKey(elementPosition)) {
                    branchRestrictions.put(elementPosition, new HashMap<Integer, Restriction>());
                }
                branchRestrictions.get(elementPosition).put(branchElement, restriction);
            }

            private boolean containsRestriction(int elementPosition) {
                return mainPathRestrictions.containsKey(elementPosition);
            }

            private boolean containsRestriction(int elementPosition, int branchElement) {
                return branchRestrictions.containsKey(elementPosition) && branchRestrictions.get(elementPosition).containsKey(branchElement);
            }

            private void removesRestriction(int elementPosition) {
                mainPathRestrictions.remove(elementPosition);
            }

            private void removesRestriction(int elementPosition, int branchElement) {
                if (branchRestrictions.containsKey(elementPosition)) {
                    branchRestrictions.get(elementPosition).remove(branchElement);
                }
            }

            private Map<Integer, Restriction> getMainPathRestrictions() {
                return mainPathRestrictions;
            }

            private Map<Integer, Restriction> getBranchRestrictions(int elementPosition) {
                return branchRestrictions.get(elementPosition);
            }
        }

        private class Identifiers {

            private Set<Integer> mainPathIdentifiers;

            private Map<Integer, Set<Integer>> branchIdentifiers;

            private Identifiers() {
                mainPathIdentifiers = new HashSet<Integer>();
                branchIdentifiers = new HashMap<Integer, Set<Integer>>();
            }

            private void clear() {
                mainPathIdentifiers.clear();
                branchIdentifiers.clear();
            }


            private void clear(int position) {
                mainPathIdentifiers.remove(position);
            }

            private void clear(int position, int branchElement) {
                branchIdentifiers.get(position).remove(branchElement);
            }

            private void addIdentifier(int elementPosition) {
                mainPathIdentifiers.add(elementPosition);
            }

            private void addIdentifier(int elementPosition, int branchElement) {
                if (!branchIdentifiers.containsKey(elementPosition)) {
                    branchIdentifiers.put(elementPosition, new HashSet<Integer>());
                }
                branchIdentifiers.get(elementPosition).add(branchElement);
            }

            private boolean containsIdentifier(int elementPosition) {
                return mainPathIdentifiers.contains(elementPosition);
            }

            private boolean containsIdentifier(int elementPosition, int branchElement) {
                return branchIdentifiers.containsKey(elementPosition) && branchIdentifiers.get(elementPosition).contains(branchElement);
            }

            private void removeIdentifier(int elementPosition) {
                mainPathIdentifiers.remove(elementPosition);
            }

            private void removeIdentifier(int elementPosition, int branchElement) {
                if (branchIdentifiers.containsKey(elementPosition)) {
                    branchIdentifiers.get(elementPosition).remove(branchElement);
                }
            }

            private Set<Integer> getMainPathIdentifiers() {
                return mainPathIdentifiers;
            }

            private Map<Integer, Set<Integer>> getBranchIdentifiers() {
                return branchIdentifiers;
            }
        }

        private final OWLBasicModel2 model;

        /**
         * List of possible starting nodes. Calculated at construction time. These are ordered by appearance in the object properties
         */
        private final List<String> startingNodes;

        private final boolean ignoreThing;

        private final List<ListAndPointer<String>> mainPath;

        private final Map<Integer, List<ListAndPointer<String>>> branches;

        /**
         * Stack that stores which elements must be modified when
         */
        private final ElementStack changingElements;

        private final Restrictions restrictions;

        private final Identifiers identifiers;

        private CurrentView(OWLBasicModel2 model, boolean ignoreThing) {
            this.model = model;
            this.ignoreThing = ignoreThing;
            startingNodes = findStartingNodes(model, ignoreThing);
            mainPath = new ArrayList<ListAndPointer<String>>();
            branches = new HashMap<Integer, List<ListAndPointer<String>>>();
            changingElements = new ElementStack();
            restrictions = new Restrictions();
            identifiers = new Identifiers();
            reset();
        }

        private void reset() {
            mainPath.clear();
            mainPath.add(new ListAndPointer<String>(startingNodes));
            String startingNode = mainPath.get(0).getElement();
            mainPath.add(new ListAndPointer<String>(getNextElements(model, startingNode, ignoreThing)));
            String property = mainPath.get(1).getElement();
            mainPath.add(new ListAndPointer<String>(getNextElements(model, property, ignoreThing)));
            branches.clear();
            changingElements.clear();
            restrictions.clear();
            identifiers.clear();
        }

        /**
         * Looks for nodes in the model with outgoing relations. These nodes serve as starting nodes for generating the views. Nodes are ordered
         * according the properties
         * <p/>
         * This method is only invoked once, at the beginning, and its result is stored during all operation
         *
         * @param model       the OWLBasicModel to analyze
         * @param ignoreThing the Thing class will not be included in the list of starting nodes
         * @return the list of classes that serve as starting node
         */
        private List<String> findStartingNodes(final OWLBasicModel2 model, boolean ignoreThing) {
            // go through all classes and consider those that have either any object properties or any datatype properties
            List<String> startingNodes = new ArrayList<String>();
            for (String class_ : model.getRDFClasses()) {
                if (!model.getObjectPropertiesForClass(class_).isEmpty() || !model.getDataTypePropertiesForClass(class_).isEmpty()) {
                    startingNodes.add(class_);
                }
            }
            // go through all object properties, to reorder them appropriately. For every pair of classes linked by a property, I must ensure that the
            // list of starting classes keeps this order (if not, switch them)
            for (String property : model.getObjectProperties()) {
                for (String domain : model.getDomainForObjectProperty(property)) {
                    for (String range : model.getRangeForObjectProperty(property)) {
                        // if both classes are in the starting class list, make sure they are correctly ordered
                        int indexOfDomain = startingNodes.indexOf(domain);
                        int indexOfRange = startingNodes.indexOf(range);
                        if (indexOfDomain >= 0 && indexOfRange >= 0 && indexOfDomain > indexOfRange) {
                            // switch positions
                            startingNodes.set(indexOfDomain, range);
                            startingNodes.set(indexOfRange, domain);
                        }
                    }
                }
            }
            if (ignoreThing) {
                startingNodes.remove("http://www.w3.org/2002/07/owl#Thing");
            }
            return startingNodes;
        }

        public View getView() {
            return buildCurrentView();
        }

        private View buildCurrentView() {
            if (mainPath.size() > 0) {
                ArrayList<Path> pathList = new ArrayList<Path>();
                ArrayList<CoordinatesBound> bounds = new ArrayList<CoordinatesBound>();
                pathList.add(buildPath(mainPath, restrictions.getMainPathRestrictions()));

                // this table stores the relation of branches positions to their corresponding path number
                // it is dynamically built when we construct the paths
                // we need it for correctly creating the identifiers
                Map<Integer, Integer> branchPositionToPathNumber = new HashMap<Integer, Integer>();
                int pathNumber = 1;
                for (int elementPosition : branches.keySet()) {
                    pathList.add(buildPath(branches.get(elementPosition), restrictions.getBranchRestrictions(elementPosition)));
                    bounds.add(new CoordinatesBound(0, internalPositionTomAPIPosition(true, elementPosition), pathList.size() - 1, 0));
                    branchPositionToPathNumber.put(elementPosition, pathNumber);
                    pathNumber++;
                }
                View view = new View(pathList, bounds);
                addIdentifiersToView(view, branchPositionToPathNumber);
                return view;
            } else {
                return null;
            }
        }

        private Path buildPath(List<ListAndPointer<String>> lists, Map<Integer, Restriction> restrictions) {
            ArrayList<Triple> triples = new ArrayList<Triple>();
            for (int i = 0; i < lists.size() - 2; i += 2) {
                triples.add(new Triple(lists.get(i).getElement(), lists.get(i + 1).getElement(), lists.get(i + 2).getElement()));
            }
            Path path = new Path(triples);
            if (restrictions != null) {
                for (int index : restrictions.keySet()) {
                    path.setRestriction(internalPositionTomAPIPosition(true, index), restrictions.get(index));
                }
            }
            return path;
        }

        private void addIdentifiersToView(View view, Map<Integer, Integer> branchPositionToPathNumber) {
            for (int index : identifiers.getMainPathIdentifiers()) {
                // the main path identifiers are associated with the path #0, since the main path is always path 0
                view.addIdentifier(new Identifier(0, internalPositionTomAPIPosition(true, index)));
            }
            for (int branchPosition : identifiers.getBranchIdentifiers().keySet()) {
                int path = branchPositionToPathNumber.get(branchPosition);
                for (int index : identifiers.getBranchIdentifiers().get(branchPosition)) {
                    view.addIdentifier(new Identifier(path, internalPositionTomAPIPosition(true, index)));
                }
            }
        }

        private static int internalPositionTomAPIPosition(boolean isClass, int position) {
            if (isClass) {
                return position / 2;
            } else {
                return (position + 1) / 2;
            }
        }

        public int currentProgress() {
            // check if we are finished
            if (mainPath.isEmpty()) {
                return 100;
            }
            // first estimate percentage with the number of starting nodes already processed
            float percentageRangeForStartingNode = (100f / (float) startingNodes.size());
            float progress = percentageRangeForStartingNode * (float) mainPath.get(0).pointer;
            if (mainPath.size() > 0) {
                // add the progress portion corresponding to the processed properties
                progress += percentageRangeForStartingNode * (float) mainPath.get(1).pointer / (float) mainPath.get(1).list.size();
            }
            // cast to int and limit to [0-100]
            int intProgress = (int) progress;
            if (intProgress > 100) {
                intProgress = 100;
            } else if (intProgress < 0) {
                intProgress = 0;
            }
            return intProgress;
        }

        private List<String> getNextElements(OWLBasicModel2 model, String element, boolean ignoreThing) {
            if (model.isRDFClass(element)) {
                List<String> properties = model.getObjectPropertiesForClass(element);
                properties.addAll(model.getDataTypePropertiesForClass(element));
                return properties;
            } else if (model.isObjectProperty(element)) {
                List<String> range = model.getRangeForObjectProperty(element);
                if (ignoreThing) {
                    range.remove("http://www.w3.org/2002/07/owl#Thing");
                }
                return model.getRangeForObjectProperty(element);
            } else if (model.isDataTypeProperty(element)) {
                List<String> datatypeRange = new ArrayList<String>();
                datatypeRange.add(model.getRangeForDataTypeProperty(element));
                return datatypeRange;
            } else {
                return null;
            }
        }

        public void nextView() {
            int elementToChange = changingElements.top();
            if (elementToChange >= 0) {
                // a branch
                if (!advanceLists(branches.get(elementToChange))) {
                    // this branch has died -> remove it and try next view again
                    removeBranch(elementToChange);
                    nextView();
                }
            } else {
                // move the main path (there are no branches)
                advanceLists(mainPath);
            }
            clearViewData();
        }

        private boolean advanceLists(List<ListAndPointer<String>> lists) {
            int originalListsSize = lists.size();
            while (!lists.isEmpty() && !lists.get(lists.size() - 1).hasMore()) {
                if (lists == mainPath) {
                    // must also remove possible branches
                    removeBranch(lists.size() - 1);
                }
                lists.remove(lists.size() - 1);
            }
            if (lists.size() > 0) {
                // if this element has a branch, it must be removed as well
                if (lists == mainPath) {
                    // must also remove possible branches
                    removeBranch(lists.size() - 1);
                }
                lists.get(lists.size() - 1).next();
            }
            refillLists(lists, originalListsSize);
            return !lists.isEmpty();
        }

        private boolean advanceLists(List<ListAndPointer<String>> lists, int position) {
            // first, we delete all elements after the specified position (we save the original size for refilling at the end)
            // we delete any existing branch at the specified position, since the element is advancing
            // finally, we advance the element at the specified position and refill the lists
            int originalListsSize = lists.size();
            while (lists.size() > position + 1) {
                if (lists == mainPath) {
                    // must also remove possible branches
                    removeBranch(lists.size() - 1);
                }
                lists.remove(lists.size() - 1);
            }
            advanceLists(lists);
            refillLists(lists, originalListsSize);
            return !lists.isEmpty();
        }

        public void nextView(int elementPosition) {
            advanceLists(mainPath, elementPosition);
            clearViewData(elementPosition);
        }

        public void nextView(int elementPosition, int branchPosition) {
            if (branchPosition == 0) {
                // the first element of the branch -> it is the element in the main path that is discarded!!!
                nextView(elementPosition);
            } else {
                // an element within a branch
                if (!advanceLists(branches.get(elementPosition), branchPosition)) {
                    // the branch is now empty -> delete it
                    removeBranch(elementPosition);
                }
                clearViewData(elementPosition, branchPosition);
            }
        }

        private void removeElementFromMainPath() {
            removeBranch(mainPath.size() - 1);
            mainPath.remove(mainPath.size() - 1);
        }

        private void refillLists(List<ListAndPointer<String>> lists, int originalListsSize) {
            // refill the lists
            boolean finished = false;
            while (!lists.isEmpty() && lists.size() < originalListsSize && !finished) {
                List<String> nextElements = getNextElements(model, lists.get(lists.size() - 1).getElement(), ignoreThing);
                if (nextElements != null && !nextElements.isEmpty()) {
                    lists.add(new ListAndPointer<String>(getNextElements(model, lists.get(lists.size() - 1).getElement(), ignoreThing)));
                } else {
                    finished = true;
                }
            }
        }

        public boolean canGeneralizeView() {
            return mainPath.size() >= 5;
        }

        public void generalizeView() throws IllegalArgumentException {
            if (canGeneralizeView()) {
                // remove two elements from the lists
                removeElementFromMainPath();
                removeElementFromMainPath();
                clearViewData();
            } else {
                throw new IllegalArgumentException("This view cannot be generalized");
            }
        }

        public boolean canGeneralizeView(int elementPosition) {
            return branches.containsKey(elementPosition);
        }

        public void generalizeView(int elementPosition) throws IllegalArgumentException {
            if (branches.containsKey(elementPosition)) {
                // there is a branch to generalize here -> remove 2 elements from it
                // if the branch is empty after this, remove the whole branch
                List<ListAndPointer<String>> branch = branches.get(elementPosition);
                branch.remove(branch.size() - 1);
                branch.remove(branch.size() - 1);
                if (branch.size() <= 1) {
                    removeBranch(elementPosition);
                }
            } else {
                throw new IllegalArgumentException("Trying to generalize a non-existent branch: " + elementPosition);
            }
            clearViewData();
        }

        public boolean canSpecifyView() {
            // checks if the main path can be further specified
            return startingNodes.contains(mainPath.get(mainPath.size() - 1).getElement());
        }

        public boolean canSpecifyView(int elementPosition) {
            // checks if two new lists can be added (looks for the referenced elements and checks if it is in the list of starting nodes)
            if (!branches.containsKey(elementPosition)) {
                // no branch at this position
                return startingNodes.contains(mainPath.get(elementPosition).getElement());
            } else {
                // already a branch here
                return startingNodes.contains(branches.get(elementPosition).get(branches.get(elementPosition).size() - 1).getElement());
            }
        }

        public void specifyView(String property) {
            String element = mainPath.get(mainPath.size() - 1).getElement();
            // there is already a branch at this position -> further specify the branch
            if (property == null) {
                // no specific property provided -> add all existing properties
                mainPath.add(new ListAndPointer<String>(getNextElements(model, mainPath.get(mainPath.size() - 1).getElement(), ignoreThing)));

            } else {
                // add only the provided property, checking if it is possible
                if ((model.isObjectProperty(property) && model.getDomainForObjectProperty(property).contains(element)) || (model.isDataTypeProperty(property) && model.getDomainForDataTypeProperty(property).contains(element))) {
                    // this property is available for the element
                    List<String> propertyList = new ArrayList<String>();
                    propertyList.add(property);
                    mainPath.add(new ListAndPointer<String>(propertyList));
                } else {
                    // error, this property is not available for the current element
                    throw new IllegalArgumentException("Property " + property + " not available for the element " + element);
                }
            }
            // add the classes from the current property at the last position
            mainPath.add(new ListAndPointer<String>(getNextElements(model, mainPath.get(mainPath.size() - 1).getElement(), ignoreThing)));
            clearViewData();
        }

        public void specifyView(int elementPosition, String property) {
            // element position must be even (correspond to a class)
            if (elementPosition % 2 != 0) {
                throw new IllegalArgumentException("Element position must correspond to a class, received " + elementPosition);
            }
            if (!branches.containsKey(elementPosition)) {
                // if the branch does not exist, create it
                String element = mainPath.get(elementPosition).getElement();
                List<ListAndPointer<String>> branchLists = new ArrayList<ListAndPointer<String>>();
                branchLists.add(new ListAndPointer<String>(element));
                branches.put(elementPosition, branchLists);
                changingElements.push(elementPosition);
            }
            // there is already a branch at this position -> further specify the branch
            if (property == null) {
                // no specific property provided -> add all existing properties
                List<ListAndPointer<String>> branch = branches.get(elementPosition);
                String element = branch.get(branch.size() - 1).getElement();
                branch.add(new ListAndPointer<String>(getNextElements(model, element, ignoreThing)));
            } else {
                // add only the provided property, checking if it is possible
                List<ListAndPointer<String>> branch = branches.get(elementPosition);
                String element = branch.get(branch.size() - 1).getElement();
                if (model.getDomainForObjectProperty(property).contains(element)) {
                    // this property is available for the element at elementPosition
                    List<String> propertyList = new ArrayList<String>();
                    propertyList.add(property);
                    branch.add(new ListAndPointer<String>(propertyList));
                } else {
                    // error, this property is not available for the current element
                    throw new IllegalArgumentException("Property " + property + " not available for the element " + element);
                }
            }
            // add the classes from the current property at the last position
            List<ListAndPointer<String>> branch = branches.get(elementPosition);
            String element = branch.get(branch.size() - 1).getElement();
            branches.get(elementPosition).add(new ListAndPointer<String>(getNextElements(model, element, ignoreThing)));
            clearViewData();
        }

        private void removeBranch(int branch) {
            branches.remove(branch);
            changingElements.remove(branch);
        }

        /**
         * Sets a restriction for an element of the main path
         *
         * @param elementPosition position of the element to restrict
         * @param restriction     restriction value
         */
        public void setRestriction(int elementPosition, Restriction restriction) {
            restrictions.addRestriction(elementPosition, restriction);
        }

        /**
         * Indicates if an element has a restriction attached to it
         *
         * @param elementPosition position to check
         * @return true if the specified element has a restriction
         */
        public boolean canRemoveRestriction(int elementPosition) {
            return restrictions.containsRestriction(elementPosition);
        }

        /**
         * Removes a restriction on an element
         *
         * @param elementPosition element position
         */
        public void removeRestriction(int elementPosition) {
            restrictions.removesRestriction(elementPosition);
        }

        /**
         * Sets a restriction for an element of a branch
         *
         * @param elementPosition position of the branch to restrict
         * @param branchElement   index of the element in the specified branch
         * @param restriction     restriction value
         */
        public void setRestriction(int elementPosition, int branchElement, Restriction restriction) {
            if (branchElement == 0) {
                setRestriction(elementPosition, restriction);
            } else {
                restrictions.addRestriction(elementPosition, branchElement, restriction);
            }
        }

        /**
         * Indicates if an element of a branch has a restriction attached to it
         *
         * @param elementPosition position to check
         * @param branchElement   element in the branch
         * @return true if the specified element has a restriction
         */
        public boolean canRemoveRestriction(int elementPosition, int branchElement) {
            if (branchElement == 0) {
                return canRemoveRestriction(elementPosition);
            } else {
            return restrictions.containsRestriction(elementPosition, branchElement);
            }
        }

        /**
         * Removes a restriction on an element of a branch
         *
         * @param elementPosition element position
         * @param branchElement   element in the branch
         */
        public void removeRestriction(int elementPosition, int branchElement) {
            if (branchElement == 0) {
                removeRestriction(elementPosition);
            } else {
                restrictions.removesRestriction(elementPosition, branchElement);
            }
        }

        /**
         * Sets an identifier for an element of the main path
         *
         * @param elementPosition position of the element to tag as identifier
         */
        public void setIdentifier(int elementPosition) {
            identifiers.addIdentifier(elementPosition);
        }

        /**
         * Indicates if an element has an identifier attached to it
         *
         * @param elementPosition position to check
         * @return true if the specified element has an identifier
         */
        public boolean canRemoveIdentifier(int elementPosition) {
            return identifiers.containsIdentifier(elementPosition);
        }

        /**
         * Removes an identifier on an element
         *
         * @param elementPosition element position
         */
        public void removeIdentifier(int elementPosition) {
            identifiers.removeIdentifier(elementPosition);
        }

        /**
         * Sets an identifier for an element of a branch
         *
         * @param elementPosition position of the branch to tag as identifier
         * @param branchElement   index of the element in the specified branch
         */
        public void setIdentifier(int elementPosition, int branchElement) {
            if (branchElement == 0) {
                setIdentifier(elementPosition);
            } else {
                identifiers.addIdentifier(elementPosition, branchElement);
            }
        }

        /**
         * Indicates if an element of a branch has an identifier attached to it
         *
         * @param elementPosition position to check
         * @param branchElement   element in the branch
         * @return true if the specified element has an identifier
         */
        public boolean canRemoveIdentifier(int elementPosition, int branchElement) {
            if (branchElement == 0) {
                return canRemoveIdentifier(elementPosition);
            } else {
                return identifiers.containsIdentifier(elementPosition, branchElement);
            }
        }

        /**
         * Removes an identifier on an element of a branch
         *
         * @param elementPosition element position
         * @param branchElement   element in the branch
         */
        public void removeIdentifier(int elementPosition, int branchElement) {
            if (branchElement == 0) {
                removeIdentifier(elementPosition);
            } else {
                identifiers.removeIdentifier(elementPosition, branchElement);
            }
        }

        private void clearViewData() {
            restrictions.clear();
            identifiers.clear();
        }

        private void clearViewData(int position) {
            restrictions.clear();
            identifiers.clear();
        }

        private void clearViewData(int position, int branchElement) {
            restrictions.clear();
            identifiers.clear();
        }

        private String serialize() {
            Document doc = new Document(new Element("currentView"));

            //STARTING NODES
            Element startingNodesElement = new Element("startingNodes");
            for(String startingNode : this.startingNodes){
                Element sN = new Element("startingNode");
                sN.addContent(startingNode);
                startingNodesElement.addContent(sN);
            }
            doc.getRootElement().addContent(startingNodesElement);

            //IGNORE THING
            Element ignoreThing = new Element("ignoreThing");
            if(this.ignoreThing)
                ignoreThing.addContent("true");
            else
                ignoreThing.addContent("false");
            doc.getRootElement().addContent(ignoreThing);

            //MAIN PATH
            Element mainPath = new Element("mainPath");
            for(CurrentView.ListAndPointer<String> lp:this.mainPath){
                Element listAndPointer = new Element("listAndPointer");
                Integer pointer = lp.pointer;
                listAndPointer.setAttribute("pointer", pointer.toString());
                for(String listElement : lp.list){
                    Element le = new Element("listElement");
                    le.addContent(listElement);
                    listAndPointer.addContent(le);
                }
                mainPath.addContent(listAndPointer);
            }
            doc.getRootElement().addContent(mainPath);

            //BRANCHES
            Element branches = new Element("branches");
            for(Map.Entry entry : this.branches.entrySet()){
                Element branch = new Element("branch");
                branch.setAttribute("key",entry.getKey().toString());
                ArrayList<CurrentView.ListAndPointer> branchListandPointer = (ArrayList<CurrentView.ListAndPointer>) entry.getValue();
                for(CurrentView.ListAndPointer<String> lp : branchListandPointer){
                    Element listAndPointer = new Element("listAndPointer");
                    Integer pointer = lp.pointer;
                    listAndPointer.setAttribute("pointer", pointer.toString());
                    for(String listElement : lp.list){
                        Element le = new Element("listElement");
                        le.addContent(listElement);
                        listAndPointer.addContent(le);
                    }
                    branch.addContent(listAndPointer);
                }
                branches.addContent(branch);
            }
            doc.getRootElement().addContent(branches);

            //CHANGING ELEMENTS
            Element changingElements = new Element("changingElements");
            for(Integer branch : this.changingElements.branches){
                Element changingBranch = new Element("branchElement");
                changingBranch.setAttribute("value",branch.toString());
                changingElements.addContent(changingBranch);
            }
            doc.getRootElement().addContent(changingElements);

            //RESTRICTIONS
            Element restrictions = new Element("restrictions");
            Element mainPathRestrictions = new Element("mainPath");
            for(Map.Entry mainPathRest : this.restrictions.mainPathRestrictions.entrySet()){
                Element rest = new Element("restriction");
                rest.setAttribute("key",mainPathRest.getKey().toString());
                Restriction restValues = (Restriction) mainPathRest.getValue();
                rest.setAttribute("op",restValues.getOp().toString());
                rest.setAttribute("value",restValues.getValue());
                rest.setAttribute("range",restValues.getRange().toString());
                mainPathRestrictions.addContent(rest);
            }
            Element branchPathRestrictions = new Element("branchPath");
            for(Map.Entry branchPathRest : this.restrictions.branchRestrictions.entrySet()){
                Element branch = new Element("branch");
                branch.setAttribute("key",branchPathRest.getKey().toString());
                HashMap<Integer,Restriction> branchRestrictions = (HashMap<Integer, Restriction>) branchPathRest.getValue();
                for(Map.Entry restrictionValues : branchRestrictions.entrySet()){
                    Element rest = new Element("restriction");
                    Restriction restValues = (Restriction) restrictionValues.getValue();
                    rest.setAttribute("position",restrictionValues.getKey().toString());
                    rest.setAttribute("op",restValues.getOp().toString());
                    rest.setAttribute("value",restValues.getValue());
                    rest.setAttribute("range",restValues.getRange().toString());
                    branch.addContent(rest);
                }
                branchPathRestrictions.addContent(branch);
            }
            restrictions.addContent(mainPathRestrictions);
            restrictions.addContent(branchPathRestrictions);
            doc.getRootElement().addContent(restrictions);

            //IDENTIFIERS
            Element identifiers = new Element("identifiers");
            Element mainPathIdentifiers = new Element("mainPath");
            for(Integer identifier: this.identifiers.getMainPathIdentifiers()){
                Element identifierNumber = new Element("identifier");
                identifierNumber.setAttribute("position",identifier.toString());
                mainPathIdentifiers.addContent(identifierNumber);
            }
            Element branchPathIdentifiers = new Element("branchPath");
            for(Map.Entry identifierBranches : this.identifiers.getBranchIdentifiers().entrySet()){
                Element branch = new Element("branch");
                branch.setAttribute("key",identifierBranches.getKey().toString());
                HashSet<Integer> branchIdentifiers = (HashSet<Integer>) identifierBranches.getValue();
                for(Integer identifierValues : branchIdentifiers){
                    Element identifierNumber = new Element("identifier");
                    identifierNumber.setAttribute("position",identifierValues.toString());
                    branch.addContent(identifierNumber);
                }
                branchPathIdentifiers.addContent(branch);
            }
            identifiers.addContent(mainPathIdentifiers);
            identifiers.addContent(branchPathIdentifiers);
            doc.getRootElement().addContent(identifiers);

            XMLOutputter xmlOutput = new XMLOutputter();
            xmlOutput.setFormat(Format.getPrettyFormat());
            return xmlOutput.outputString(doc);
        }

        private void deSerialize(String url) throws JDOMException, IOException {
            SAXBuilder builder = new SAXBuilder();
            Document currentViewDoc = builder.build(url);
            Element rootNode = currentViewDoc.getRootElement();

            //STARTING NODES
            Element startingNodesElement = rootNode.getChild("startingNodes");
            startingNodes.clear();
            for(Element startingNode : startingNodesElement.getChildren()){
                startingNodes.add(startingNode.getValue());
            }

            //IGNORE THING
            //TODO Ask Albeto what is this
            /*Element ignoreThingElement = rootNode.getChild("ignoreThing");
            final boolean auxIgnoreThing = (ignoreThingElement.getValue() == "true");
            this.ignoreThing = true; */

            //MAINPATH
            Element mainPathElement = rootNode.getChild("mainPath");
            mainPath.clear();
            for(Element mainListAndPointer : mainPathElement.getChildren()){
                List<String> pointerList = new ArrayList<>();
                for(Element pointerListElement : mainListAndPointer.getChildren()){
                    pointerList.add(pointerListElement.getValue());
                }
                //TODO CHAPUCERIO MAXIMO CAMBIALO
                ListAndPointer lp = new ListAndPointer(pointerList,new ArrayList());
                lp.pointer = mainListAndPointer.getAttribute("pointer").getIntValue();
                mainPath.add(lp);
            }

            //BRANCHES
            Element branchPathElement = rootNode.getChild("branches");
            branches.clear();
            for(Element branch : branchPathElement.getChildren()){
                List<ListAndPointer<String>> branchListAndPointer = new ArrayList<>();
                for(Element listAndPointer : branch.getChildren()){
                    ListAndPointer lp = new ListAndPointer(listAndPointer.getValue().trim());
                    lp.pointer = listAndPointer.getAttribute("pointer").getIntValue();
                    branchListAndPointer.add(lp);
                }
                branches.put(branch.getAttribute("key").getIntValue(),branchListAndPointer);
            }

            //CHANGING ELEMENTS
            Element changingElement = rootNode.getChild("changingElements");
            changingElements.clear();
            for(Element changingBranch : changingElement.getChildren()){
                changingElements.push(changingBranch.getAttribute("value").getIntValue());
            }

            //RESTRICTIONS
            Element restrictionElements = rootNode.getChild("restrictions");
            // >> MAINPATH
            Element mainPathRestrictions = restrictionElements.getChild("mainPath");
            restrictions.mainPathRestrictions.clear();
            for(Element restriction : mainPathRestrictions.getChildren()){
                //GET OPERATOR
                Restriction.Operator op = Restriction.Operator.valueOf(restriction.getAttributeValue("op"));
                //GET RANGE
                Restriction.Range range = Restriction.Range.valueOf(restriction.getAttributeValue("range"));
                //GET VALUE
                String value = restriction.getAttributeValue("value");
                //ADDING RESTRICTION
                restrictions.mainPathRestrictions.put(restriction.getAttribute("key").getIntValue(),new Restriction(op,value,range));
            }
            // >> BRANCHES
            Element branchPathRestrictions = restrictionElements.getChild("branchPath");
            restrictions.branchRestrictions.clear();
            for(Element branch : branchPathRestrictions.getChildren()){
                Map<Integer,Restriction> auxRestrictions = new HashMap<>();
                for(Element restriction : branch.getChildren()){
                    //GET OPERATOR
                    Restriction.Operator op = Restriction.Operator.valueOf(restriction.getAttributeValue("op"));
                    //GET RANGE
                    Restriction.Range range = Restriction.Range.valueOf(restriction.getAttributeValue("range"));
                    //GET VALUE
                    String value = restriction.getAttributeValue("value");
                    //ADDING RESTRICTION
                    auxRestrictions.put(restriction.getAttribute("position").getIntValue(),new Restriction(op,value,range));
                }
                restrictions.branchRestrictions.put(branch.getAttribute("key").getIntValue(),auxRestrictions);
            }

            //IDENTIFIERS
            Element identifierElements = rootNode.getChild("identifiers");
            // >> MAINPATH
            Element mainPathIdentifiers = identifierElements.getChild("mainPath");
            identifiers.mainPathIdentifiers.clear();
            for(Element identifier : mainPathIdentifiers.getChildren()){
                identifiers.mainPathIdentifiers.add(identifier.getAttribute("position").getIntValue());
            }
            // >> BRANCHES
            Element branchPathIdentifiers = identifierElements.getChild("branchPath");
            identifiers.branchIdentifiers.clear();
            for(Element branch : branchPathIdentifiers.getChildren()){
                Set<Integer> identifierSet = new HashSet<>();
                for(Element identifier : branch.getChildren()){
                    identifierSet.add(identifier.getAttribute("position").getIntValue());
                }
                identifiers.branchIdentifiers.put(branch.getAttribute("key").getIntValue(),identifierSet);
            }

        }
    }

    private static String SERIALIZING_MARK = "\n######\n";

    /**
     * Object for generating views
     */
    private CurrentView currentView;

    /**
     * Views that have been used by the user for mapping them
     */
    private final Set<View> mappedViews;

    /**
     * Discarded views are stored so they are not returned twice. When the current view is specified or generalized, it is added to the
     * discarded views
     */
    private final Set<View> discardedViews;

    /**
     * Keeps a sequence of every action made by PhysicalViewGenerator
     */
    private ActionSequence actionSequence;

    /**
     * Class constructor
     *
     * @param model       owl basic model from which the views must be generated
     * @param ignoreThing whether the Thing class should be ignored when generating views
     */
    public PhysicalViewGenerator(OWLBasicModel2 model, boolean ignoreThing) {
        currentView = new CurrentView(model, ignoreThing);
        mappedViews = new HashSet<View>();
        discardedViews = new HashSet<View>();
        actionSequence = new ActionSequence();
    }

    /**
     * Returns the currently generated view
     *
     * @return the current view (null if all possible views have been generated)
     */
    public View currentView() {
        View view = currentView.getView();
        if (!discardedViews.contains(view) && !mappedViews.contains(view)) {
            return view;
        } else {
            currentView.nextView();
            return currentView();
        }
    }

    /**
     * Sets a restriction for an element of the main path
     *
     * @param elementPosition position of the element to restrict
     * @param restriction     restriction value
     */
    public void setRestriction(int elementPosition, Restriction restriction) {
        String sRestriction = restriction.getOp().toString()+"##"+restriction.getValue()+"##STRING";
        actionSequence.addActionToSequence(ActionSequence.ActionName.SET_RESTRICTION_INT_RESTRICTION,String.valueOf(elementPosition),sRestriction);
        currentView.setRestriction(mAPIIndexToGeneratorIndex(true, elementPosition), restriction);
    }

    public boolean canRemoveRestriction(int elementPosition) {
        return currentView.canRemoveRestriction(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    public void removeRestriction(int elementPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.REMOVE_RESTRICTION_INT,String.valueOf(elementPosition));
        currentView.removeRestriction(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    /**
     * Sets a restriction for an element of a branch
     *
     * @param elementPosition position of the branch to restrict
     * @param branchElement   index of the element in the specified branch
     * @param restriction     restriction value
     */
    public void setRestriction(int elementPosition, int branchElement, Restriction restriction) {
        String sRestriction = restriction.getOp().toString()+"##"+restriction.getValue()+"##STRING";
        actionSequence.addActionToSequence(ActionSequence.ActionName.SET_RESTRICTION_INT_INT_RESTRICTION,String.valueOf(elementPosition),String.valueOf(branchElement).toString(),sRestriction);
        currentView.setRestriction(mAPIIndexToGeneratorIndex(true, elementPosition), mAPIIndexToGeneratorIndex(true, branchElement), restriction);
    }

    public boolean canRemoveRestriction(int elementPosition, int branchElement) {
        return currentView.canRemoveRestriction(mAPIIndexToGeneratorIndex(true, elementPosition), mAPIIndexToGeneratorIndex(true, branchElement));
    }

    public void removeRestriction(int elementPosition, int branchElement) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.REMOVE_RESTRICTION_INT_INT,String.valueOf(elementPosition),String.valueOf(branchElement));
        currentView.removeRestriction(mAPIIndexToGeneratorIndex(true, elementPosition), mAPIIndexToGeneratorIndex(true, branchElement));
    }

    /**
     * Sets an identifier for an element of the main path
     *
     * @param elementPosition position of the element to tag as identifier
     */
    public void setIdentifier(int elementPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.SET_IDENTIFIER_INT,String.valueOf(elementPosition));
        currentView.setIdentifier(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    public boolean canRemoveIdentifier(int elementPosition) {
        return currentView.canRemoveIdentifier(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    public void removeIdentifier(int elementPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.REMOVE_IDENTIFIER_INT,String.valueOf(elementPosition));
        currentView.removeIdentifier(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    /**
     * Sets an identifier for an element of a branch
     *
     * @param elementPosition position of the branch to tag as identifier
     * @param branchElement   index of the element in the specified branch
     */
    public void setIdentifier(int elementPosition, int branchElement) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.SET_IDENTIFIER_INT_INT,String.valueOf(elementPosition),String.valueOf(branchElement));
        currentView.setIdentifier(mAPIIndexToGeneratorIndex(true, elementPosition), mAPIIndexToGeneratorIndex(true, branchElement));
    }

    public boolean canRemoveIdentifier(int elementPosition, int branchElement) {
        return currentView.canRemoveIdentifier(mAPIIndexToGeneratorIndex(true, elementPosition), mAPIIndexToGeneratorIndex(true, branchElement));
    }

    public void removeIdentifier(int elementPosition, int branchElement) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.REMOVE_IDENTIFIER_INT_INT,String.valueOf(elementPosition),String.valueOf(branchElement));
        currentView.removeIdentifier(mAPIIndexToGeneratorIndex(true, elementPosition), mAPIIndexToGeneratorIndex(true, branchElement));
    }

    /**
     * Returns the estimated progress completed for the provided model
     *
     * @return an integer value indicating the estimated percentage completed (between 0 and 100)
     */
    public int currentProgress() {
        return currentView.currentProgress();
    }

    /**
     * Evaluates if the current view can be generalized
     *
     * @return true if the current view can be generalized (has more than 1 triple)
     */
    public boolean canGeneralizeView() {
        return currentView.canGeneralizeView();
    }

    /**
     * Reduces the size of the main path
     *
     * @throws IllegalArgumentException the view cannot be generalized
     */
    public void generalizeView() throws IllegalArgumentException {
        actionSequence.addActionToSequence(ActionSequence.ActionName.GENERALIZE_VIEW);
        discardedViews.add(currentView());
        currentView.generalizeView();
    }

    /**
     * Evaluates if the current view can be generalized at a specific position
     *
     * @param elementPosition position in the main path
     * @return true if there is a branch at the specified position, false otherwise
     */
    public boolean canGeneralizeView(int elementPosition) {
        return currentView.canGeneralizeView(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    /**
     * Generalizes the current view at a specific position (reduces or eliminates an existing branch at that location)
     *
     * @param elementPosition position in the main path to generalize
     * @throws IllegalArgumentException if there is no branch at the specified position
     */
    public void generalizeView(int elementPosition) throws IllegalArgumentException {
        actionSequence.addActionToSequence(ActionSequence.ActionName.GENERALIZE_VIEW_INT,String.valueOf(elementPosition));
        discardedViews.add(currentView());
        currentView.generalizeView(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    /**
     * Evaluates if it is possible to grow the main path
     *
     * @return true if the main path can be grown
     */
    public boolean canSpecifyView() {
        return currentView.canSpecifyView();
    }

    /**
     * Grows the main path with all possible properties
     */
    public void specifyView() {
        //actionSequence.addActionToSequence(ActionSequence.ActionName.SPECIFY_VIEW);
        specifyView(null);
    }

    /**
     * Grows the main path with one additional property
     *
     * @param property property to add to the end of the main path
     */
    public void specifyView(String property) {
        // specify the main path with a specific property
        actionSequence.addActionToSequence(ActionSequence.ActionName.SPECIFY_VIEW_STRING,property);
        discardedViews.add(currentView());
        currentView.specifyView(property);
    }

    /**
     * Evaluates the possibility of specifying the main path at one specific main path location
     *
     * @param elementPosition position of the main path
     * @return true if the main path can be specified at the given position
     */
    public boolean canSpecifyView(int elementPosition) {
        return currentView.canSpecifyView(mAPIIndexToGeneratorIndex(true, elementPosition));
    }

    /**
     * Creates a branch from any of the intermediate main path classes (or further specifies an existing branch)
     *
     * @param elementPosition position in the main path to specify
     */
    public void specifyView(int elementPosition) {
        //actionSequence.addActionToSequence(ActionSequence.ActionName.SPECIFY_VIEW_INT,String.valueOf(elementPosition));
        specifyView(elementPosition, null);
    }

    /**
     * Creates a branch from any of the intermediate main path classes. The first property of that branch can be specified. If a branch at this
     * element already existed, it further specifies such branch
     *
     * @param elementPosition position of the element from which the branch is created
     * @param property        property for the new branch (null if all possible properties must be used)
     */
    public void specifyView(int elementPosition, String property) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.SPECIFY_VIEW_INT_STRING,String.valueOf(elementPosition),property);
        discardedViews.add(currentView());
        currentView.specifyView(mAPIIndexToGeneratorIndex(true, elementPosition), property);
    }

    /**
     * The current view is accepted. The next view will be generated
     */
    public void acceptView(){
        actionSequence.addActionToSequence(ActionSequence.ActionName.ACCEPT_VIEW);
        //A new sequence must start for the new view
        actionSequence.endViewSequence();
        mappedViews.add(currentView());
        currentView.nextView();
    }

    /**
     * The current view is discarded. The next view will be generated
     */
    public void discardView() {
        actionSequence.addActionToSequence(ActionSequence.ActionName.DISCARD_VIEW);
        //A new sequence must start for the new view
        actionSequence.endViewSequence();
        discardedViews.add(currentView());
        currentView.nextView();
    }

    /**
     * The current view is discarded, specifying the position in the main path which we want to advance
     *
     * @param classPosition position in the main path to be modified
     */
    public void discardClass(int classPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.DISCARD_CLASS_INT,String.valueOf(classPosition));
        discardedViews.add(currentView());
        currentView.nextView(mAPIIndexToGeneratorIndex(true, classPosition));
    }

    public void discardClass(int classPosition, int branchPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.DISCARD_CLASS_INT_INT,String.valueOf(classPosition),String.valueOf(branchPosition));
        discardedViews.add(currentView());
        currentView.nextView(mAPIIndexToGeneratorIndex(true, classPosition), mAPIIndexToGeneratorIndex(true, branchPosition));
    }

    /**
     * The current view is discarded, specifying the position in the main path which we want to advance
     *
     * @param propertyPosition position in the main path to be modified
     */
    public void discardProperty(int propertyPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.DISCARD_PROPERTY_INT,String.valueOf(propertyPosition));
        discardedViews.add(currentView());
        currentView.nextView(mAPIIndexToGeneratorIndex(false, propertyPosition));
    }

    public void discardProperty(int classPosition, int branchPosition) {
        actionSequence.addActionToSequence(ActionSequence.ActionName.DISCARD_PROPERTY_INT_INT,String.valueOf(classPosition),String.valueOf(branchPosition));
        discardedViews.add(currentView());
        currentView.nextView(mAPIIndexToGeneratorIndex(true, classPosition),  mAPIIndexToGeneratorIndex(false, branchPosition));
    }

    public String serialize(String fileName) {
        // serialize as a string value, so it can be easily stored in a database. The string should resist version updates, so the stored
        // projects do not have to be dumped each time we update something
        String serializedProgress = actionSequence.serialize();

        try{
            File serializeOutput = new File(fileName);
            FileWriter fw = new FileWriter(serializeOutput.getAbsoluteFile());
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(serializedProgress);
            bw.close();
        }catch(IOException e){
            e.printStackTrace();
        }
        return serializedProgress;
    }

    public void deSerialize(String str) throws IOException, JDOMException {
        mappedViews.clear();
        discardedViews.clear();
        //TODO: THIS IS MODEL ADDHOC
        currentView = new CurrentView(currentView.model, true);
        actionSequence.deSerialize(this,str);
    }

    private int mAPIIndexToGeneratorIndex(boolean isClass, int position) {
        int pos = position * 2;
        if (!isClass) {
            pos--;
        }
        return pos;
    }
}
